#include "settingsImport.hpp"
#include "settingsFunctions.hpp"
#include "version.hpp"
#include "simulationTime.hpp"
#include "typeHelpers.hpp"
#include "INIReader_extension.hpp"
#include "util.hpp"
#include "streamEigen.hpp"
#include "vtkFunctions.hpp"
#include "vtkFunctions_import.hpp"
#include "loggerFunctions.hpp"

#include <iostream>
#include <iomanip>
#include <stdexcept>

// #ifdef __linux__
#include <cstdlib>
// #endif

namespace hms
{
using std::string_literals::operator""s;

ini::INIReader getReader( const fs::path& iniPath ){

	ini::INIReader reader { iniPath.string() };
	
	if ( reader.ParseError() < 0)
	{
		throw FileError( fmt::format(
			"{}:\n{}\"{}\"",
			bold(red("Could not find configuration file")),
			indent(),
			bold( iniPath.string() )
		) );
	}
	return reader;
}

fs::path iniPath( const fs::path& caseDir ){
	return { caseDir / Settings::dir / Settings::file };
}

void setReader( Settings& s ){
	s.iniReader = getReader( iniPath( s.caseDir() ) );
}



#define GET_OR_LEAVE_DEFAULT(SECTION, OBJECT, WHAT) \
ini::read( reader, SECTION, HMS_TO_STRING(WHAT), OBJECT.WHAT )

#define GET_ENUM_OR_DEFAULT(SECTION, OBJECT, WHAT) \
readEnumOrDefault( reader, SECTION, HMS_TO_STRING(WHAT), OBJECT.WHAT )



log::Settings getLogSettings(
	const ini::INIReader& reader,
	const fs::path& caseDir
){
	static constexpr auto section ("log");
	log::Settings s;

	GET_ENUM_OR_DEFAULT( section, s, logLevel_console );

	GET_OR_LEAVE_DEFAULT(section, s, useCustomLogFormat_console);
	if ( s.useCustomLogFormat_console ){
		GET_OR_LEAVE_DEFAULT(section, s, customLogFormat_console);
	}

	GET_OR_LEAVE_DEFAULT(section, s, useLogFile);
	if ( s.useLogFile ){
		GET_ENUM_OR_DEFAULT( section, s, logLevel_file );
		/* default is case directory */
		std::string logDirStr { caseDir.string() };
		/* then we read the setting */
		ini::read(reader, section, "logDirectory", logDirStr);
		s.logDirectory = fs::path{logDirStr};
		/* if an absolute directory was given, it's left as is, otherwise it's
		 * interpreted as a subdirectory of the case directory.
		 * No check is performed whether or not it exists.
		 * The logging library can create directories. */
		s.logDirectory = subdirOrAbsolute( caseDir, s.logDirectory.string() );

		GET_OR_LEAVE_DEFAULT(section, s, logFileName);
		GET_OR_LEAVE_DEFAULT(section, s, appendLog);
		GET_OR_LEAVE_DEFAULT(section, s, useLogFileRotation);
		if ( s.useLogFileRotation ){
			GET_OR_LEAVE_DEFAULT(section, s, logRotationMaxFiles);
			GET_OR_LEAVE_DEFAULT(section, s, logRotationMaxSizeKB);
		}
		GET_OR_LEAVE_DEFAULT(section, s, useCustomLogFormat_file);
		if ( s.useCustomLogFormat_file ){
			GET_OR_LEAVE_DEFAULT(section, s, customLogFormat_file);
		}
		GET_OR_LEAVE_DEFAULT(section, s, useAsyncLogger);
	}

	return s;
}

log::Settings getLogSettings( const hms::Settings& s){
	return getLogSettings( s.iniReader, s.caseDir() );
}



bool getBenchmarkFlag( const ini::INIReader& reader ){
	bool runBenchmark {false};
	ini::read( reader, "general", "runBenchmark", runBenchmark );
	return runBenchmark;
}


void setCaseName( Settings& s ){
	/* if a benchmark case is run, set case name to benchmark case name,
	 * otherwise read specified name from ini and default to case dir name */
	s.runBenchmarkCase = getBenchmarkFlag( s.iniReader );
	std::string caseName;
	if ( s.runBenchmarkCase ){
		BenchmarkSettings b { getBenchmarkSettings(s.iniReader) };
		caseName = magic_enum::enum_name(b.benchCase);
	}
	else {
		ini::read( s.iniReader, "general", "caseName", caseName );
		if ( caseName.empty() )
			caseName = s.caseDir().filename().string();
	}
	s.caseName( std::move(caseName) );

	s.logger().debug( "Case name: \"{}\"", s.caseName() );
}


BenchmarkSettings getBenchmarkSettings( const ini::INIReader& reader ){
	using BenchCase = typename BenchmarkSettings::BenchCase;
	constexpr auto section { "benchmark" };
	BenchmarkSettings b;

	GET_OR_LEAVE_DEFAULT(section, b, exportBenchmarkResults);

	readEnumOrDefault<BenchCase>(reader, section, "case", b.benchCase);

	b.setValid();

	return b;
}

MeshGenSettings getMeshGenSettings( const ini::INIReader& reader ){
	using MS = MeshGenSettings;
	using MeshType  = typename MS::MeshType;

	MS ms;

	constexpr auto section { "meshGen" };

	GET_OR_LEAVE_DEFAULT(section, ms, useCellCount);
	GET_OR_LEAVE_DEFAULT(section, ms, useCellSize);
	if ( !ms.useCellCount && !ms.useCellSize )
		throw InputError( fmt::format(
			"{}: Cannot generate mesh, "
			"when useCellCount=false {} useCellSize=false",
			red("Conflicting mesh generation settings"),
			bold("and")
		) );

	if ( ms.useCellCount ){
		GET_OR_LEAVE_DEFAULT(section, ms, nCellsX);
		GET_OR_LEAVE_DEFAULT(section, ms, nCellsY);
	}
	if ( ms.useCellSize ){
		GET_OR_LEAVE_DEFAULT(section, ms, dX);
		GET_OR_LEAVE_DEFAULT(section, ms, dY);
	}
	if ( !ms.useCellCount || !ms.useCellSize ){
		GET_OR_LEAVE_DEFAULT(section, ms, sizeX);
		GET_OR_LEAVE_DEFAULT(section, ms, sizeY);
	}

	GET_OR_LEAVE_DEFAULT(section, ms, originX);
	GET_OR_LEAVE_DEFAULT(section, ms, originY);
	GET_OR_LEAVE_DEFAULT(section, ms, addBoundaryPatches);
	GET_OR_LEAVE_DEFAULT(section, ms, useStandardBoundaryPatches);
	readEnumOrDefault<MeshType>(reader, section, "MeshType", ms.meshType );

	if ( ms.addBoundaryPatches && !ms.useStandardBoundaryPatches ){
		Index nPatches {0};
		ini::read(reader, section, "nBoundaryPatches", nPatches);
		log::debug( "Reading {} custom boundary patches...", nPatches );

		std::vector<typename MS::PatchSettings> ps (nPatches);
		std::string subsection;

		auto readCoords = [&](std::string vert, Vector2s& coords){
			std::string coordsStr { reader.Get(subsection, vert, {}) };

			std::stringstream conv { coordsStr };
			hms::read( conv, coords);
		};

		for ( Index i{0}; i<nPatches; ++i ){
			subsection = fmt::format("boundaryPatch{}", i+1);
			ini::read(reader, subsection, "name",  ps[i].name);
			readCoords("beg", ps[i].beg);
			readCoords("end", ps[i].end);
			ini::read(reader, subsection, "clockwise", ps[i].clockwise);
			log::trace(
				"{}Patch #{}, section [{}], name=\"{}\", "
				"beg=({}), end=({}), clockwise={}",
				indent(),
				i+1, subsection, ps[i].name,
				ps[i].beg.transpose(), ps[i].end.transpose(), ps[i].clockwise
			);
		}
		ms.boundaryPatches = std::move(ps);
	}

	ms.setValid();

	return ms;
}

void setBenchmarkSettings( Settings& s ){
	s.runBenchmarkCase = getBenchmarkFlag( s.iniReader );

	std::shared_ptr<log::logger> logger { log::getLogger() };

	if ( s.runBenchmarkCase ){
		logger->debug("Reading benchmark settings...");
		s.benchmark( getBenchmarkSettings(s.iniReader) );
		logger->debug("Reading mesh generation settings...");
		s.meshGen( getMeshGenSettings(s.iniReader) );
	}
}


bool getPrintLogoFlag( const ini::INIReader& reader ){
	return reader.GetBoolean("general", "printLogo", false);
}

void setTimeSettings( Settings& s ){
	static constexpr auto section {"time"};

	s.logger().debug("Reading time settings...");

	TimeSettings t;

	ini::read( s.iniReader, section, "startTime"  , t.start  );
	ini::read( s.iniReader, section, "endTime"    , t.end    );
	t.maxTimeStep = t.end - t.start;
	ini::read( s.iniReader, section, "maxTimeStep", t.maxTimeStep);

	enum class WriteAt {
		timeInterval,
		stepInterval
	};

	auto writeAt { readEnumOrDefault<WriteAt>(s.iniReader, section, "writeAt") };

	if ( writeAt == WriteAt::timeInterval ){
		t.writeAtTimeIntervals = true;
		ini::read( s.iniReader, section, "writeTimeInterval", t.writeTimeInterval);
	} else if ( writeAt == WriteAt::stepInterval ){
		t.writeAtTimeIntervals = false;
		ini::read( s.iniReader, section, "writeStepInterval", t.writeStepInterval);
	}

	ini::read(s.iniReader, section,
		"useExplicitInitialTimeStep",
		 t.useExplicitFirstStep
	);

	if ( t.useExplicitFirstStep ){
		ini::read(s.iniReader, section, "initialTimeStep", t.initialStep);
	}

	#ifndef NDEBUG
	ini::read(s.iniReader, section, "maxNTimeSteps", t.maxSteps);
	#endif

	t.setValid();
	s.time( std::move(t) );
}

WhichSolver whichSolver( const ini::INIReader& reader ){
	constexpr auto section { "solver" };
	return readEnumOrDefault<WhichSolver>(
		reader, section, "whichSolver"
	);
}


template<typename SolverType>
void readSolverSettings(
	const ini::INIReader& reader,
	SolverSettings<SolverType>& s,
	const std::string& derivedSection,
	bool integratedBlockLoop
){
	using magic_enum::enum_name;
	using SettingsType = SolverSettings<SolverType>;
	using SourceFieldName = typename SettingsType::SourceFieldName;

	constexpr auto section { "solver" };

	for ( unsigned i{0}; i<s.useSourceFields.size(); ++i ){
		s.useSourceFields[i] = false;
		ini::read(reader, derivedSection,
			"use" + std::string{enum_name( static_cast<SourceFieldName>(i) )},
			s.useSourceFields[i]
		);
	}

	if (!integratedBlockLoop){
		GET_ENUM_OR_DEFAULT(section, s, edgeTraversal);
	}
}

SWESettings getSWESettings( const ini::INIReader& reader ){
	using magic_enum::enum_name;

	constexpr auto section { "swe" };
	SWESettings s;

	GET_OR_LEAVE_DEFAULT(section, s, useIntegratedBlockLoop);
	readSolverSettings(reader, s, section, s.useIntegratedBlockLoop);

	GET_OR_LEAVE_DEFAULT(section, s, useFriction);
	GET_OR_LEAVE_DEFAULT(section, s, maxCourant);

	if( s.maxCourant <= 0 || s.maxCourant > 1 )
		throw InputError( fmt::format(
			"Invalid setting: [swe] maxCourant={}"
			"\n{}Courant number must be between 0 and 1!",
			bold(red("{}", s.maxCourant)),
			indent()
		) );

	GET_ENUM_OR_DEFAULT(section, s, limiterFunction);

	if (s.useIntegratedBlockLoop){
		GET_OR_LEAVE_DEFAULT(section, s, setBlocksizesManually);
		if (s.setBlocksizesManually){
			GET_OR_LEAVE_DEFAULT(section, s, blocksizeX);
			GET_OR_LEAVE_DEFAULT(section, s, blocksizeY);
			auto checkSize = [&](Index size, std::string_view setting){
				if ( size < 1 )
					throw InputError( fmt::format(
						"Invalid setting: [{}] {}={}"
						"\n{}Block sizes must be positive integers!",
						section, setting, bold(red("{}", size)),
						indent()
					) );
			};
			checkSize( s.blocksizeX, "blocksizeX" );
			checkSize( s.blocksizeY, "blocksizeY" );
			if (
				   (s.blocksizeX == 1 && s.blocksizeY != 1)
				|| (s.blocksizeX != 1 && s.blocksizeY == 1)
			){
				throw InputError( fmt::format(
					"Setting combination not handled:"
					"\n{0}[{1}] {2} and"
					"\n{0}[{1}] {3}"
					"\nEither both block sizes must be 1 or both must be greater than 1.",
					indent(), section,
					bold(yellow("blocksizeX={}",s.blocksizeX)),
					bold(yellow("blocksizeY={}",s.blocksizeY))
				) );
			}
		} else {
			GET_ENUM_OR_DEFAULT(section, s, blocksizeAdjustment );
			if ( s.blocksizeAdjustment == swe::RemAdjust::threshold )
				GET_OR_LEAVE_DEFAULT(section, s, blocksizeThreshold);
		}
	} else {
		GET_ENUM_OR_DEFAULT(section, s, fluxScheme);
		GET_OR_LEAVE_DEFAULT(section, s, useMuscl);
		GET_OR_LEAVE_DEFAULT(section, s, useAtomicWrite);

		std::string_view et { enum_name( s.edgeTraversal ) };
		/* not sure about the capitalisation staying the way it is,
		* so I removed the 's' from "second" */
		if ( s.useMuscl == (et.find("econdOrder") == std::string_view::npos) ){
			throw InputError( fmt::format(
				"{}: chosen numerical scheme (useMuscl={}) "
				"doesn't match chosen traversal scheme (edgeTraversal={})",
				red("Conflicting solver settings"),
				bold("{}", s.useMuscl),
				bold("{}", et)
			) );
		}
	}

	s.setValid();
	return s;
}

void setSWESettings( Settings& s ){
	s.logger().debug("Reading solver settings...");
	s.swe( getSWESettings( s.iniReader ) );
}

std::vector<ProbeSettings> getProbeSettings( const ini::INIReader& reader ){
	constexpr auto section { "export" };

	Index nProbes {0};
	ini::read(reader, section, "nProbes", nProbes);

	std::vector<ProbeSettings> p (nProbes);
	std::string subsection;

	for ( Index i{0}; i<nProbes; ++i ){
		subsection = fmt::format("probe{}", i+1);
		ini::read(reader, subsection, "enabled",  p[i].enabled);
		if ( !p[i].enabled )
			continue;

		p[i].fileName = subsection + ".csv";

		ini::read(reader, subsection, "fileName",  p[i].fileName);
		ini::read(reader, subsection, "colSep", p[i].colSep);

		/* to allow specifying whitespace separators, strip quotes */
		std::stringstream ss {p[i].colSep};
		ss >> quoted(p[i].colSep);
		// std::cout << "col sep of probe #" << i << ": \"" << p[i].colSep << "\"\n";

		Index nPoints {0};
		ini::read(reader, subsection, "nPoints",  nPoints);

		p[i].coords.resize(2, nPoints);

		std::string coordsStr { reader.Get(subsection, "coords", {}) };
		// std::cout << "Coords string: " << coordsStr << '\n';

		std::stringstream conv { coordsStr };
		hms::read( conv, p[i].coords);
		// std::cout << "coords:\n" << p.coords << '\n';
	}
	return p;
}

void setProbeSettings( Settings& s ){
	s.logger().debug("Reading probe settings...");
	s.probes( getProbeSettings(s.iniReader) );
}



bool getResumeSimFlag( const ini::INIReader& reader ){

	enum class StartAt {
		startTime,
		last
	};

	auto startAt { readEnumOrDefault<StartAt>( reader, "time", "startAt" ) };

	return startAt == StartAt::last;
}

fs::path getBindingFilePath( Settings& s ){
	return vtk::getBindingFilePath( s.caseDir(), s.caseName() );
}

void setStartTime( Settings& s ){
	/* check whether to start or resume the simulation */
	s.resumeSimulation = getResumeSimFlag(s.iniReader);

	log::logger& logger { s.logger() };

	if ( s.resumeSimulation ){
		TimeSettings& t { s.time() };
		/* guard against binding file not existing */
		if ( fs::is_regular_file( s.bindingFilePath() ) ){
			t.start = vtk::lastWrittenTime( s.caseDir(), s.caseName() );
			logger.debug( "Simulation start time set to t={}s.", t.start );
		} else {
			s.resumeSimulation = false;
			logger.warn(
				"Requested to continue simulation, "
				"but VTK binding file"
				"\n{}\"{}\"\n"
				"was not found. "
				"Defaulting to starting from time t={}s.",
				indent(),
				s.bindingFilePath().string(),
				t.start
			);
		}
	}
	logger.debug( "Start time set to {}s", s.time().start );
}


namespace import
{
using MeshFormat = ImportSettings::MeshFormat;
using RasterFormat = ImportSettings::RasterFormat;

fs::path& existsOrThrow( fs::path& file ){
	if ( fs::is_regular_file(file) )
		return file;
	else
		throw FileError( fmt::format(
			"{}:\n{}\"{}\"",
			bold(red("Import file not found")),
			indent(),
			bold( "{}", file.string() )
		) );
}

fs::path& replaceExtension( MeshFormat format, fs::path& meshfile ){
	if ( !meshfile.has_extension() ){
		if ( format == MeshFormat::gmsh ){
			meshfile.replace_extension( ".msh" );
		} else {
			throw std::logic_error( fmt::format(
				"No mesh file extension assigned to format \"{}\"",
				bold(red( magic_enum::enum_name(format) ))
			) );
		}
	}
	return existsOrThrow(meshfile);
}

fs::path& replaceExtension( RasterFormat format, fs::path& rasterfile ){
	if ( !rasterfile.has_extension() ){
		if ( format == RasterFormat::esri ){
			return rasterfile.replace_extension( ".asc" );
		} else {
			throw std::logic_error( fmt::format(
				"No raster file extension assigned to format \"{}\"",
				bold(red( magic_enum::enum_name(format) ))
			) );
		}
	}
	return existsOrThrow(rasterfile);
}

template<typename FormatType>
void importItem(
	const ini::INIReader& reader,
	ImportSettings& is,
	typename ImportSettings::ImportItem<FormatType>& item,
	const std::string& itemSection,
	const std::string& flagName,
	bool readInterpolation
){
	using Flag = ImportSettings::Flag;
	using RasterFormat = ImportSettings::RasterFormat;

	// constexpr auto section { "import" };

	/* accept all bool values that the ini lib allows,
	 * as well as the enum values */
	bool flag {item.import != Flag::never}, flagFound;
	flagFound = ini::read(reader, itemSection, flagName, flag, false);
	if (flagFound){
		if (flag)
			item.import = Flag::always;
		else
			item.import = Flag::never;
	} else {
		item.import = readEnumOrDefault<Flag>(reader, itemSection, flagName);
	}

	if ( item.import != Flag::never ){
		/* ascertain that import dir exists */
		if ( !fs::is_directory(is.importPath) )
			throw FileError( fmt::format(
				"{}:\n{}\"{}\"",
				bold("Import directory doesn't exist"),
				indent(), is.importPath.string()
			) );

		item.format = readEnumOrDefault<FormatType>(
			reader, itemSection, "format"
		);

		std::string filename;
		ini::read( reader, itemSection, "file", filename );
		item.file = is.importPath / fs::path{std::move(filename)};

		/* account for custom raster formats */
		bool isCustom {false};
		if constexpr ( std::is_same_v<FormatType, RasterFormat> ){
			if ( item.format == FormatType::custom ){
				isCustom = true;
				std::string formatFile =
					reader.Get( itemSection, "formatFile", "" );
				if ( formatFile.empty() )
					throw ParseError( fmt::format(
						"{}\n"
						"For custom raster formats, configuration entry "
						"\"{}\" must be specified!\n"
						"The format file must be provided in the same directory"
						" as the raster file.\n"
						"The full filename {} extension must be provided.",
						bold(red("Format file for custom raster format not specified!")),
						bold("[{}] formatFile", itemSection),
						bold("with")
					) );
				item.formatFile = is.importPath / formatFile;
				import::existsOrThrow( item.formatFile );
			}
		}
		/* for known formats, the extension can be added automatically */
		if (!isCustom)
			import::replaceExtension( item.format, item.file );

		if ( readInterpolation ){
			GET_OR_LEAVE_DEFAULT(itemSection, item, interpolate);
		}
	}
}

} // namespace import

void setImportSettings( Settings& settings ){
	constexpr auto section { "import" };

	settings.logger().info("Reading import settings...");

	const ini::INIReader& reader { settings.iniReader };

	ImportSettings is;

	/* get import dir first, because it is used for each import item */
	std::string importDir;
	ini::read(reader, section, "importDir", importDir );
	is.importPath = fs::path{importDir};
	
	if ( is.importPath.empty() )
		is.importPath = fs::path{ section };
	if ( is.importPath.is_relative() )
		is.importPath = settings.caseDir() / is.importPath;

	assert( settings.importItem.has_value() );

	using SI = typename Settings::ImportItem;
	SI what { settings.importItem.value() };

	using import::importItem;

	/* mesh settings must always be read,
	 * because the mesh is needed for all types of import. */
	importItem(reader, is, is.mesh, "mesh", "importMesh", false);
	GET_OR_LEAVE_DEFAULT("mesh", is, generateMesh);
	GET_OR_LEAVE_DEFAULT("mesh", is, optimiseMeshType);

	if ( what == SI::all || what == SI::dem ){
		importItem(reader, is, is.dem , "dem" , "importDEM", false );
	}
	if ( what == SI::all || what == SI::rainfall ){
		importItem(reader, is, is.rain, "rain", "importRain", true);
	}
	if ( what == SI::all || what == SI::roughness ){
		importItem(reader, is, is.roughness,
			"roughness", "importRoughness", false);
	}

	/* check for conflicting settings */
	using Flag = ImportSettings::Flag;
	if (
		!is.generateMesh
		&& is.mesh.import == Flag::never
		&& is.dem.import == Flag::never
		&& is.rain.import == Flag::never
		&& is.roughness.import == Flag::never
	){
		throw InputError( fmt::format(
			"{}: Option \"{}\" specified, "
			"but all import settings in ini set to \"{}\".",
			red("Conflicting mesh settings"),
			bold("{}", "--import"),
			bold("{}", "never" )
		) );
	}

	if ( is.generateMesh && is.mesh.import != ImportSettings::Flag::never ){
		throw InputError( fmt::format(
			"{}: generateMesh={} and"
			"importMesh={}",
			red("Conflicting mesh settings"),
			bold("{}", "true"),
			bold("{}", magic_enum::enum_name(is.mesh.import) )
		) );
	}

	is.setValid();

	settings.import( std::move(is) );
}


ExportSettings getExportSettings( const ini::INIReader& reader ){
	constexpr auto section {"export"};
	ExportSettings es;
	GET_OR_LEAVE_DEFAULT(section, es, exportResults);

	es.format = readEnumOrDefault<typename ExportSettings::Format>(
		reader, section, "exportFormat"
	);

	es.setValid();

	return es;
}

void setExportSettings( Settings& s ){
	s.logger().debug("Reading export settings...");
	s.exportSettings( getExportSettings( s.iniReader ) );
}

void readIni( Settings& settings ){
	setReader(settings);

	createLogger(settings);
	
	if ( !settings.quiet && getPrintLogoFlag(settings.iniReader) )
		printLogo();

	log::logger& logger { settings.logger() };

	log::printSection(logger, "Reading ini", log::Level::debug);

	logger.debug("Reading settings from configuration file:\n  {}",
		iniPath( settings.caseDir() ).string()
	);

	setCaseName(settings);

	/* set paths derived from case dir, case name, and constants */
	setDerivedPaths(settings);

	setTimeSettings(settings);

	logger.debug("Reading solver type...");
	if ( whichSolver(settings.iniReader) != WhichSolver::swe )
		throw InputError("Only SWE solver is implemented!");

	setSWESettings(settings);

	setProbeSettings(settings);

	setStartTime( settings );

	setExportSettings(settings);
}


#undef GET_OR_LEAVE_DEFAULT
#undef GET_ENUM_OR_DEFAULT


} // namespace hms
